package ec.cd.client;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.UnknownHostException;
import java.net.Socket;
import java.util.concurrent.ExecutionException;

import android.os.AsyncTask;
import android.os.Vibrator;
import android.content.Context;
import android.util.Log;
import android.widget.TextView;

import ec.cd.common.Networking.BodyLocation;
import ec.cd.common.Networking.ClientHello;
import ec.cd.common.Networking.ClientSendData;
import ec.cd.common.Networking.Message;
import ec.cd.common.Networking.MessageType;
import ec.cd.common.Networking.ServerCalibrationRun;
import ec.cd.common.Networking.ServerNormalRun;
import static ec.cd.common.Networking.receiveMessage;
import static ec.cd.common.Networking.sendMessage;
import ec.cd.common.Sensors.SensorData;
import ec.cd.common.Sensors.SensorRunningMode;

public class Networking {
    public static class Client {
        private static String TAG = "Networking.Client";

        public static interface ConnectionEventListener {
            public void disconnected();
        }

        public Client(Context context, TextView logger, SensorsManager sm,
                      ConnectionEventListener cel) {
            this.context = context;
            loggerTextView = logger;
            sensorsManager = sm;
            connectionEventListener = cel;
            continueDataTransfer = false;
        }

        // Return true on success, false otherwise
        public boolean connect(String host, int port) {
            disconnect();

            try {
                socket = new Socket(host, port);
                objOutputStream = new ObjectOutputStream(socket.getOutputStream());
                objInputStream = new ObjectInputStream(socket.getInputStream());
                
                return true;
            } catch (UnknownHostException e) {
                Log.e(TAG, e.toString());
            } catch (IOException e) {
                Log.e(TAG, e.toString());
            }

            socket = null;

            return false;
        }

        public void disconnect() {
            if (connected()) {
                continueDataTransfer = false;

                try {
                    handleConnectionTask.get(); // Wait for completion
                } catch (InterruptedException e) {
                    Log.e(TAG, e.toString());
                } catch (ExecutionException e) {
                    Log.e(TAG, e.toString());
                }
                handleConnectionTask = null;
            }
        }

        public boolean connected() {
            return socket != null;
        }

        public void handleConnection(BodyLocation bl) {
            continueDataTransfer = true;
            handleConnectionTask = new HandleConnectionTask().execute(bl);
        }

        private class HandleConnectionTask extends AsyncTask<BodyLocation, String, Boolean> {
            protected Boolean doInBackground(BodyLocation... bls) {
                try {
                    // ClientHello
                    publishProgress("Sending greeting message to server\n");
                    Message outgoing = new Message();
                    outgoing.mt = MessageType.ClientHello;
                    outgoing.payload = new ClientHello();
                    ((ClientHello) outgoing.payload).bl = bls[0];
                    sendMessage(objOutputStream, outgoing);

                    // ServerNormalRun/ServerCalibrationRun
                    publishProgress("Waiting for server command\n");
                    Message incoming = receiveMessage(objInputStream);
                    switch (incoming.mt) {
                        case ServerCalibrationRun:
                            publishProgress("Going to calibration mode\n");
                            return doCalibrationRun(
                                    (ServerCalibrationRun)incoming.payload);
                        case ServerNormalRun:
                            publishProgress("Going to normal run mode\n");
                            return doNormalRun(
                                    (ServerNormalRun)incoming.payload);
                        default:
                            publishProgress("Unexpected server message: " +
                                            incoming.mt + "\n");
                    }
                } catch (IOException e) {
                    Log.e(TAG, e.toString());
                } catch (ClassNotFoundException e) {
                    Log.e(TAG, e.toString());
                } catch (ClassCastException e) {
                    Log.e(TAG, e.toString());
                }

                return false;
            }

            protected void onProgressUpdate(String... msgs) {
                loggerTextView.append(msgs[0]);
            }

            protected void onPostExecute(Boolean res) {
                loggerTextView.append(res ? "Communication succeeded\n"
                                          : "Communication failed\n");

                sensorsManager.disableAllSensors();
                try {
                    socket.close();
                } catch (IOException e) {
                    Log.e(TAG, e.toString());
                }
                socket = null;

                connectionEventListener.disconnected();
            }

            // Return number of sensor events sent, -1 on error
            private int sendQueuedData() {
                SensorData[] sensors_data =
                    sensorsManager.getSensorData().toArray(new SensorData[0]);
                int data_len = sensors_data.length;
                if (data_len == 0)
                    return 0;

                ClientSendData csd = new ClientSendData();
                csd.sensors_data = sensors_data;
                Message outgoing = new Message();
                outgoing.mt = MessageType.ClientSendData;
                outgoing.payload = csd;

                try {
                    sendMessage(objOutputStream, outgoing);
                    sensorsManager.getSensorData().clear();

                    return data_len;
                } catch (IOException e) {
                    Log.e(TAG, e.toString());
                }

                return -1;
            }

            // Calibration run
            private boolean doCalibrationRun(ServerCalibrationRun scr) {
                if (scr == null || scr.phaseNames == null) {
                    Log.e(TAG, "Invalid ServerCalibrationRun message");
                    return false;
                }

                publishProgress(
                        "Enabling all sensors at the fastest sampling rate\n");
                sensorsManager.enableSensors(SensorRunningMode.Calibration,
                                             null);

                int sendingRate = 1000;
                try {
                    publishProgress("Sending sensor data every " +
                                    sendingRate + "ms\n");
                    for (String phase : scr.phaseNames) {
                        if (!continueDataTransfer)
                            break;

                        publishProgress("Calibrating context: " +
                                        phase + "\n");
                        ((Vibrator)context.getSystemService(
                            Context.VIBRATOR_SERVICE)).vibrate(300);

                        int total = 0;
                        while (continueDataTransfer &&
                               (total < scr.testLength)) {
                            Thread.sleep(sendingRate);
                            int data_len = sendQueuedData();
                            if (data_len == -1) {
                                publishProgress(
                                        "Failed to send all sensor data, " +
                                        (scr.testLength-total) +
                                        " packets remaining\n");
                                return false;
                            }
                            else total += data_len;
                        }

                        if (continueDataTransfer) {
                            publishProgress(
                                    "Finished sending calibration data\n");
                            Message outgoing = new Message();
                            outgoing.mt = MessageType.ClientBye;
                            outgoing.payload = new Integer(1);
                            sendMessage(objOutputStream, outgoing);
                        }
                    }

                    if (continueDataTransfer)
                        publishProgress(
                                "All sensor data sent, ending session\n");
                    else publishProgress(
                            "Session terminated by user's request\n");

                    return true;
                } catch (InterruptedException e) {
                    Log.e(TAG, e.toString());
                } catch (IOException e) {
                    Log.e(TAG, e.toString());
                }

                return false;
            }

            // Normal or PowerSave run
            private boolean doNormalRun(ServerNormalRun ssc) {
                if (ssc == null || ssc.scs == null) {
                    Log.e(TAG, "Invalid ServerNormalRun message");
                    return false;
                }

                publishProgress("Enabling sensors\n");
                sensorsManager.enableSensors(ssc.srm, ssc.scs);

                try {
                    publishProgress("Sending sensor data every " +
                                    ssc.sending_rate + "ms\n");
                    while (continueDataTransfer) {
                        Thread.sleep(ssc.sending_rate);
                        if (sendQueuedData() == -1) {
                            publishProgress(
                                    "Failed to send sensor data, exiting\n");
                            return false;
                        }
                    }

                    return true;
                } catch (InterruptedException e) {
                    Log.e(TAG, e.toString());
                }

                return false;
            }
        }

        private Socket socket;
        private ObjectOutputStream objOutputStream;
        private ObjectInputStream objInputStream;
        private Context context;
        private TextView loggerTextView;
        private SensorsManager sensorsManager;
        private ConnectionEventListener connectionEventListener;
        private AsyncTask handleConnectionTask;
        private boolean continueDataTransfer;
    }
}
